fix: community access to public-project organizations - #3808
Conversation
Extends the organization view floor to public-project organizations so community and direct-permission users can view them and set them as their preferred org — without a dedicated role. Opening a public project auto-switches the preferred org; for non-members this previously 403'd because org access only recognized stored membership/permission rows. - Access floor (canUserViewStrictOrPublic): direct permission / stored org role OR the org owns a publicly visible project. Anything beyond viewing still requires MEMBER+ via @RequiresOrganizationRole. - Reduced preferred-org model below MEMBER: activeCloudSubscription hidden; new PrivateOrganizationModel.limitedView drives the community project-list view. - Guest-visible surface scoped to accessible projects: org GETs (role null), project listings, languages/base-languages, glossary reads incl. CSV export (one canonical ProjectRepository.BELOW_MEMBER_ACCESSIBLE_PROJECT predicate), llm-providers (name/type/token-credit pricing only), /leave. - Preference healing writes on stale/missing preference; below-member viewers excluded from member listings, seat stats, and assignee search. - No GUEST role: the earlier virtual-role apparatus was removed; community and direct-permission users share below-member access via the floor.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (16)
💤 Files with no reviewable changes (5)
🚧 Files skipped from review as they are similar to previous changes (11)
📝 WalkthroughWalkthroughCommunity access now recognizes public-project organizations and below-member permissions across organization, project, language, glossary, and preferred-organization flows. Models expose limited-view state, clients adapt their UI accordingly, and backend, EE, unit, and Cypress tests cover access boundaries and stale preference recovery. ChangesCommunity access and visibility
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
webapp/src/component/layout/BaseSettingsView/SettingsMenuItem.tsx (1)
37-62: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick winUse unique
data-cyvalues instead of introducingdata-cy-item. The current contract forces tests to combine a genericdata-cywith an unsupported secondary selector attribute.
webapp/src/component/layout/BaseSettingsView/SettingsMenuItem.tsx#L37-L62: render the supplied unique selector directly throughdata-cy.webapp/src/component/layout/BaseSettingsView/SettingsMenu.tsx#L30-L30: rename and forward the prop as the component’sdata-cyvalue.webapp/src/views/organizations/components/BaseOrganizationSettingsView.tsx#L58-L98: provide complete unique selectors such assettings-menu-item-profile,settings-menu-item-glossaries, andsettings-menu-item-translation-memories.As per coding guidelines, “STRICTLY use
data-cyattributes for E2E selectors, never rely on text content; use typed helpersgcy()orcy.gcy().”🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@webapp/src/component/layout/BaseSettingsView/SettingsMenuItem.tsx` around lines 37 - 62, Replace the secondary dataCyItem selector in SettingsMenuItem with the supplied unique value rendered directly as data-cy. In SettingsMenu, rename and forward its prop as the component’s data-cy value. In BaseOrganizationSettingsView, provide complete unique selectors for each menu entry, including the profile, glossaries, and translation memories items, and keep E2E selectors based exclusively on data-cy.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/organizationController/OrganizationFloorAccessTest.kt`:
- Around line 117-128: Replace the manual JSON parsing and generic collection
assertions in the organization floor-access tests, including orgLanguageTags and
the corresponding range, with andAssertThatJson assertions. Assert the language
tags through JSON paths using the existing response assertion DSL, preserving
the current expected values and request behavior while retaining JSON-path
diagnostics.
In `@backend/data/src/main/kotlin/io/tolgee/repository/ProjectRepository.kt`:
- Around line 71-78: Update BELOW_MEMBER_ACCESSIBLE_PROJECT so its permission
existence check excludes Permission rows whose access level is NONE, matching
the existing rejection logic elsewhere. Ensure only non-NONE permissions satisfy
the user/project access condition, while preserving public visibility handling.
In `@e2e/cypress/e2e/projects/communityPreferredOrganization.cy.ts`:
- Line 33: Replace the text-based project selectors around the Community Alpha
interactions with typed data-cy hooks on the target project items, then select
them exclusively through gcy() or cy.gcy(). Add distinct stable hooks for each
affected fixture and preserve the existing click/assertion behavior without
relying on rendered project names.
In `@webapp/src/component/layout/BaseSettingsView/SettingsMenuItem.tsx`:
- Line 37: Replace the SettingsMenuItem dataCyItem selector prop and its
rendered attribute with the required data-cy convention, generating a unique
value such as settings-menu-item-profile for each menu item. Update the related
selector usage to call gcy() or cy.gcy() and ensure all affected menu items
expose unique data-cy values without relying on text content.
In `@webapp/src/views/organizations/components/BaseOrganizationSettingsView.tsx`:
- Line 58: Update the menu entry selectors in the organization settings view,
including the entries around profile and the other referenced locations, to use
complete values such as settings-menu-item-profile through the existing data-cy
contract. Ensure the emitted data-cy-item identifiers are explicit and follow
the typed gcy()/cy.gcy() selector convention without relying on menu text.
---
Outside diff comments:
In `@webapp/src/component/layout/BaseSettingsView/SettingsMenuItem.tsx`:
- Around line 37-62: Replace the secondary dataCyItem selector in
SettingsMenuItem with the supplied unique value rendered directly as data-cy. In
SettingsMenu, rename and forward its prop as the component’s data-cy value. In
BaseOrganizationSettingsView, provide complete unique selectors for each menu
entry, including the profile, glossaries, and translation memories items, and
keep E2E selectors based exclusively on data-cy.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 15919387-85ad-4ae6-b1cd-66fd4424afdd
⛔ Files ignored due to path filters (1)
webapp/src/service/apiSchema.generated.tsis excluded by!**/*.generated.*
📒 Files selected for processing (54)
backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/UserPreferencesController.ktbackend/api/src/main/kotlin/io/tolgee/api/v2/controllers/V2UserController.ktbackend/api/src/main/kotlin/io/tolgee/api/v2/controllers/organization/OrganizationLanguageController.ktbackend/api/src/main/kotlin/io/tolgee/component/PreferredOrganizationFacade.ktbackend/api/src/main/kotlin/io/tolgee/hateoas/organization/PrivateOrganizationModel.ktbackend/api/src/main/kotlin/io/tolgee/hateoas/organization/PrivateOrganizationModelAssembler.ktbackend/app/src/test/kotlin/io/tolgee/api/v2/controllers/PreferredOrganizationCommunityTest.ktbackend/app/src/test/kotlin/io/tolgee/api/v2/controllers/PublicProjectsControllerTest.ktbackend/app/src/test/kotlin/io/tolgee/api/v2/controllers/UserPreferencesSetPreferredOrganizationTest.ktbackend/app/src/test/kotlin/io/tolgee/api/v2/controllers/organizationController/OrganizationCommunityAccessTest.ktbackend/app/src/test/kotlin/io/tolgee/api/v2/controllers/organizationController/OrganizationFloorAccessTest.ktbackend/app/src/test/kotlin/io/tolgee/service/RefreshPreferredOrganizationTest.ktbackend/app/src/test/kotlin/io/tolgee/unit/PrivateOrganizationModelAssemblerTest.ktbackend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/TestDataService.ktbackend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/builders/LanguageBuilder.ktbackend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/builders/OrganizationBuilder.ktbackend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/builders/ProjectBuilder.ktbackend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/builders/TestDataBuilder.ktbackend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/GlossaryGuestAccessTestData.ktbackend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/PublicProjectsControllerTestData.ktbackend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/PublicProjectsE2eData.ktbackend/data/src/main/kotlin/io/tolgee/model/enums/OrganizationRoleType.ktbackend/data/src/main/kotlin/io/tolgee/repository/ProjectRepository.ktbackend/data/src/main/kotlin/io/tolgee/repository/UserAccountRepository.ktbackend/data/src/main/kotlin/io/tolgee/service/organization/OrganizationRoleService.ktbackend/data/src/main/kotlin/io/tolgee/service/project/ProjectService.ktbackend/data/src/main/kotlin/io/tolgee/service/security/UserPreferencesService.ktbackend/data/src/test/kotlin/io/tolgee/unit/OrganizationRoleTypeTest.ktbackend/development/src/main/kotlin/io/tolgee/controllers/internal/e2eData/PublicProjectsE2eDataController.ktbackend/security/src/main/kotlin/io/tolgee/security/authorization/OrganizationAuthorizationInterceptor.ktbackend/security/src/test/kotlin/io/tolgee/security/authorization/OrganizationAuthorizationInterceptorTest.kte2e/cypress/e2e/projects/communityPreferredOrganization.cy.tse2e/cypress/e2e/projects/communityProjectsNavigation.cy.tse2e/cypress/e2e/projects/publicProjects.cy.tse2e/cypress/support/dataCyType.d.tsee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryController.ktee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryRepository.ktee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryTermRepository.ktee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryExportService.ktee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryService.ktee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryTermService.ktee/backend/tests/src/test/kotlin/io/tolgee/ee/api/v2/controllers/AuthProviderChangeControllerEeTest.ktee/backend/tests/src/test/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryGuestAccessTest.ktwebapp/src/component/layout/BaseSettingsView/SettingsMenu.tsxwebapp/src/component/layout/BaseSettingsView/SettingsMenuItem.tsxwebapp/src/fixtures/__tests__/organizationRole.test.tswebapp/src/fixtures/organizationRole.tswebapp/src/globalContext/useInitialDataService.tswebapp/src/globalContext/useOrganizationUsageService.tswebapp/src/views/organizations/OrganizationProfileView.tsxwebapp/src/views/organizations/components/BaseOrganizationSettingsView.tsxwebapp/src/views/projects/ProjectListView.tsxwebapp/src/views/projects/members/component/MemberItem.tsxwebapp/src/views/projects/members/component/RevokePermissionsButton.tsx
The member-management leak check parsed the MockMvc response by hand and asserted doesNotContain on the extracted usernames, which would also pass against an empty list. Anchor the array as non-empty and assert per-element via andAssertThatJson.
| private val organizationRoleService: OrganizationRoleService, | ||
| private val projectService: ProjectService, | ||
| ) { | ||
| private fun accessibleProjectIds( |
There was a problem hiding this comment.
I would put it in a service (maybe SecurityService?).
The accessibleProjectIds helper lived as a private function on OrganizationLanguageController, which pulled AuthenticationFacade, OrganizationRoleService and ProjectService into the controller just to make an access decision. Move it to SecurityService as getAccessibleProjectIdsInOrganization so the controller only depends on SecurityService and the scoping rule is reusable.
A NONE permission is a revocation (no effective scopes) but the below-member floor treated any permission row as access: a user whose only tie to an org was a NONE permission on a private project could read that project's languages and glossary terms and retain org standing, even though the project-listing query already excludes NONE. Exclude NONE consistently in BELOW_MEMBER_ACCESSIBLE_PROJECT (languages + glossary) and in the org-standing queries (canUserView / findAllPermitted / findPreferred), matching the listing. Also: add discriminating floor/glossary tests for the NONE behaviour, drop a now-dead glossary accessor, minor cleanups, an OpenAPI boolean-example fix, and trim over-long comments to single-line invariants.
1344351 to
bfe364f
Compare
## [3.213.1](v3.213.0...v3.213.1) (2026-07-20) ### Bug Fixes * community access to public-project organizations ([#3808](#3808)) ([6a7f73f](6a7f73f)), closes [#3770](#3770) [#3792](#3792) [#3765](#3765) [#3792](#3792) [#3792](#3792) * keep websocket connection alive when a subscription is denied ([#3809](#3809)) ([1235ad1](1235ad1)), closes [#3529](#3529) [#3621](#3621) [#3621](#3621)
Re-lands the organization-permissions lane of Community Translation v1, which is missing from
main.The problem
Opening a public project partially fails. The frontend sets the preferred organization to the project's owner org on every project open (
ProjectContext.tsx→PUT /v2/user-preferences/set-preferred-organization/{id}), and that call is rejected for community users.mainstill has the strict check inUserPreferencesController:which resolves to
OrganizationRepository.canUserView, whose predicate is(perm is not null or mr is not null)— it requires a storedPermissionorOrganizationRolerow. The community floor from #3770 is computed by design, so contributors don't consume a paid seat. No row →PermissionException(USER_CANNOT_VIEW_THIS_ORGANIZATION)→ 403.How it went missing
#3792 was merged into
jirikuchynka/public-projectson 2026-07-13 (merge commit93e6fd9cc). That branch was subsequently rebased and force-pushed; the rebase dropped the merge commit along with its two payload commits. #3765 then squash-merged the rebased branch intomainas4a4ea3f55, shipping v1 without this lane. GitHub still reports #3792 as MERGED, which is why it went unnoticed.This PR
git cherry-pick -m 1 93e6fd9ccontomain— applied with no conflicts, and the resulting diff againstmainis identical to the original payload (55 files, 2197+/129-).Contents (unchanged from #3792):
OrganizationRoleService.canUserViewStrictOrPublic/checkUserCanViewOrPublic— the org view floor: strict check ORprojectService.hasPublicProjects(organizationId)UserPreferencesController:61andOrganizationAuthorizationInterceptor:75PreferredOrganizationFacadeheals stale/missing preferences in place and emitsPrivateOrganizationModel.limitedViewfor below-member viewersVerification
:data:/:api:/:security:/:development:compileKotlin— BUILD SUCCESSFULUserPreferencesSetPreferredOrganizationTest— 7/7 passedOrganizationFloorAccessTest— 29/29 passedeslint --fixandtsc— clean, no files changedSummary by CodeRabbit
limitedViewflag and safer subscription-related visibility behavior.